#include "ibm.h"
#include "cpu.h"
#include "model.h"
#include "io.h"
#include "fdd.h"
#include "mouse.h"
#include "mem.h"
#include "device.h"
#include "t3100e.h"
#include "vid_t3100e.h"

/* The Toshiba 3100e is a 286-based portable.
 *
 * To bring up the BIOS setup screen hold down the 'Fn' key on booting
 *
 * Memory management
 * ~~~~~~~~~~~~~~~~~
 *
 *      Motherboard memory is divided into:
 *      - Conventional memory: Either 512k or 640k
 *      - Upper memory:        Either 512k or 384k, depending on amount of
 *                             conventional memory. Upper memory can be
 *                             used either as EMS or XMS.
 *      - High memory:         0-4Mb, depending on RAM installed. The BIOS
 *                             setup screen allows some or all of this to be
 *                             used as EMS; the remainder is XMS.
 *
 *      Additional memory (either EMS or XMS) can also be provided by ISA
 *      expansion cards.
 *
 *      Under test in PCEM, the BIOS will boot with up to 65368Kb of memory in
 *      total (16Mb less 16k). However it will give an error with RAM sizes
 *      above 8Mb, if any of the high memory is allocated as EMS, because the
 *      builtin EMS page registers can only access up to 8Mb.
 *
 *      Memory is controlled by writes to I/O port 8084h:
 *        Bit 7: Always 0  }
 *        Bit 6: Always 1  } These bits select which motherboard function to
 *        Bit 5: Always 0  } access.
 *        Bit 4: Set to treat upper RAM as XMS
 *        Bit 3: Enable external RAM boards?
 *        Bit 2: Set for 640k conventional memory, clear for 512k
 *        Bit 1: Enable RAM beyond 1Mb.
 *        Bit 0: Enable EMS.
 *
 *      The last value written to this port is saved at 0040:0093h, and in
 *      CMOS memory at offset 0x37. If the top bit of the CMOS byte is set,
 *      then high memory is being provided by an add-on card rather than the
 *      mainboard; accordingly, the BIOS will not allow high memory to be
 *      used as EMS.
 *
 *      EMS is controlled by 16 page registers:
 *
 *      Page mapped at		0xD000	0xD400	0xD800	0xDC00
 *	------------------------------------------------------
 * 	Pages 0x00-0x7F		 0x208	0x4208	0x8208	0xc208
 *	Pages 0x80-0xFF		 0x218	0x4218	0x8218	0xc218
 *	Pages 0x100-0x17F	 0x258	0x4258	0x8258	0xc258
 *	Pages 0x180-0x1FF	 0x268	0x4268	0x8268	0xc268
 *
 *	The value written has bit 7 set to enable EMS, reset to disable it.
 *
 *	So: OUT 0x208,  0x80  will page in the first 16k page at 0xD0000.
 *	    OUT 0x208,  0x00  will page out EMS, leaving nothing at 0xD0000.
 *	    OUT 0x4208, 0x80  will page in the first 16k page at 0xD4000.
 *	    OUT 0x218,  0x80  will page in the 129th 16k page at 0xD0000.
 *
 *  etc.
 *
 * To use EMS from DOS, you will need the Toshiba EMS driver (TOSHEMM.ZIP).
 * This supports the above system, plus further ranges of ports at
 * 0x_2A8, 0x_2B8, 0x_2C8.
 *
 */

static const int t3100e_log = 0;

extern uint8_t *ram; /* Physical RAM */

/* Features not implemented:
 * > Four video fonts.
 * > BIOS-controlled mapping of serial ports to IRQs.
 * > Custom keyboard controller. This has a number of extra commands in the
 *   0xB0-0xBC range, for such things as turbo on/off, and switching the
 *   keyboard between AT and PS/2 modes. Currently I have only implemented
 *   command 0xBB, so that self-test completes successfully. Commands include:
 *
 *   0xB0:   Turbo on
 *   0xB1:   Turbo off
 *   0xB2:   Internal display on?
 *   0xB3:   Internal display off?
 *   0xB5:   Get settings byte (bottom bit is colour / mono setting)
 *   0xB6:   Set settings byte
 *   0xB7:   Behave as 101-key PS/2 keyboard
 *   0xB8:   Behave as 84-key AT keyboard
 *   0xBB:   Return a byte, bit 2 is Fn key state, other bits unknown.
 *
 * The other main I/O port needed to POST is:
 *      0x8084: System control.
 *              Top 3 bits give command, bottom 5 bits give parameters.
 *               000 => set serial port IRQ / addresses
 *                      bit 4:    IRQ5 serial port base: 1 => 0x338, 0 => 0x3E8
 *                      bits 3, 2, 0 specify serial IRQs for COM1, COM2, COM3:
 *			    00 0 => 4, 3, 5
 *                          00 1 => 4, 5, 3
 *                          01 0 => 3, 4, 5
 *                          01 1 => 3, 5, 4
 *                          10 0 => 4, -, 3
 *                          10 1 => 3, -, 4
 *               010 => set memory mappings
 *                         bit 4 set if upper RAM is XMS
 *			   bit 3 enable add-on memory boards beyond 5Mb?
 *                         bit 2 set for 640k sysram, clear for 512k sysram
 *                         bit 1 enable mainboard XMS
 *                         bit 0 enable mainboard EMS
 *               100 => set parallel mode / LCD settings
 *                         bit 4 set for bidirectional parallel port
 *                         bit 3 set to disable internal CGA
 *                         bit 2 set for single-pixel LCD font
 *                         bits 0,1 for display font
 */

void at_init();

/* The T3100e motherboard can (and does) dynamically reassign RAM between
 * conventional, XMS and EMS. This translates to monkeying with the mappings.
 */

extern mem_mapping_t base_mapping;

extern mem_mapping_t ram_low_mapping; /* This is to switch conventional RAM
                                       * between 512k and 640k */

extern mem_mapping_t ram_mid_mapping; /* This will not be used */

extern mem_mapping_t ram_high_mapping; /* This is RAM beyond 1Mb if any */

extern mem_mapping_t ram_remapped_mapping; /* This will not be used */

extern uint8_t *ram;

static unsigned t3100e_ems_page_reg[] = {
        0x208,
        0x4208,
        0x8208,
        0xc208, /* The first four map the first 2Mb */
        /* of RAM into the page frame */
        0x218,
        0x4218,
        0x8218,
        0xc218, /* The next four map the next 2Mb */
        /* of RAM */
        0x258,
        0x4258,
        0x8258,
        0xc258, /* and so on. */
        0x268,
        0x4268,
        0x8268,
        0xc268,
};

struct t3100e_ems_regs {
        uint8_t page[16];
        mem_mapping_t mapping[4];
        uint32_t page_exec[4]; /* Physical location of memory pages */
        uint32_t upper_base;   /* Start of upper RAM */
        uint8_t upper_pages;   /* Pages of EMS available from upper RAM */
        uint8_t upper_is_ems;  /* Upper RAM is EMS? */
        mem_mapping_t upper_mapping;
        uint8_t notify; /* Notification from keyboard controller */
        uint8_t turbo;  /* 0 for 6MHz, else full speed */
        uint8_t mono;   /* Emulates PC/AT 'mono' motherboard switch */
                        /* Bit 0 is 0 for colour, 1 for mono */
} t3100e_ems;

void t3100e_ems_out(uint16_t addr, uint8_t val, void *p);

/* Given a memory address (which ought to be in the page frame at 0xD0000),
 * which page does it relate to? */
static int addr_to_page(uint32_t addr) {
        if ((addr & 0xF0000) == 0xD0000) {
                return ((addr >> 14) & 3);
        }
        return -1;
}

/* And vice versa: Given a page slot, which memory address does it
 * correspond to? */
static uint32_t page_to_addr(int pg) { return 0xD0000 + ((pg & 3) * 16384); }

/* Given an EMS page ID, return its physical address in RAM. */
uint32_t t3100e_ems_execaddr(struct t3100e_ems_regs *regs, int pg, uint16_t val) {
        uint32_t addr;

        if (!(val & 0x80))
                return 0; /* Bit 7 reset => not mapped */

        val &= 0x7F;
        val += (0x80 * (pg >> 2)); /* The high bits of the register bank */
        /* are used to extend val to allow up */
        /* to 8Mb of EMS to be accessed */

        /* Is it in the upper memory range? */
        if (regs->upper_is_ems) {
                if (val < regs->upper_pages) {
                        addr = regs->upper_base + 0x4000 * val;
                        return addr;
                }
                val -= regs->upper_pages;
        }
        /* Otherwise work down from the top of high RAM (so, the more EMS,
         * the less XMS) */
        if ((val * 0x4000) + 0x100000 >= (mem_size * 1024)) {
                return 0; /* Not enough high RAM for this page */
        }
        /* High RAM found */
        addr = (mem_size * 1024) - 0x4000 * (val + 1);

        return addr;
}

/* The registers governing the EMS ports are in rather a nonintuitive order */
static int port_to_page(uint16_t addr) {
        switch (addr) {
        case 0x208:
                return 0;
        case 0x4208:
                return 1;
        case 0x8208:
                return 2;
        case 0xC208:
                return 3;
        case 0x218:
                return 4;
        case 0x4218:
                return 5;
        case 0x8218:
                return 6;
        case 0xC218:
                return 7;
        case 0x258:
                return 8;
        case 0x4258:
                return 9;
        case 0x8258:
                return 10;
        case 0xC258:
                return 11;
        case 0x268:
                return 12;
        case 0x4268:
                return 13;
        case 0x8268:
                return 14;
        case 0xC268:
                return 15;
        }
        return -1;
}

/* Used to dump the memory mapping table, for debugging
void dump_mappings()
{
        mem_mapping_t *mm = base_mapping.next;

        if (!t3100e_log) return;
        while (mm)
        {
                const char *name = "";
                uint32_t offset = (uint32_t)(mm->exec - ram);

                if (mm == &ram_low_mapping ) name = "LOW ";
                if (mm == &ram_mid_mapping ) name = "MID ";
                if (mm == &ram_high_mapping) name = "HIGH";
                if (mm == &t3100e_ems.upper_mapping) name = "UPPR";
                if (mm == &t3100e_ems.mapping[0])
                {
                        name = "EMS0";
                        offset = t3100e_ems.page_exec[0];
                }
                if (mm == &t3100e_ems.mapping[1])
                {
                        name = "EMS1";
                        offset = t3100e_ems.page_exec[1];
                }
                if (mm == &t3100e_ems.mapping[2])
                {
                        name = "EMS2";
                        offset = t3100e_ems.page_exec[2];
                }
                if (mm == &t3100e_ems.mapping[3])
                {
                        name = "EMS3";
                        offset = t3100e_ems.page_exec[3];
                }

                pclog("  %p | base=%05x size=%05x %c @ %06x %s\n", mm,
                        mm->base, mm->size, mm->enable ? 'Y' : 'N',
                        offset, name);

                mm = mm->next;
        }
}*/

void t3100e_map_ram(uint8_t val) {
        int n;
        int32_t upper_len;

        if (t3100e_log) {
                pclog("OUT 0x8084, %02x [ set memory mapping :", val | 0x40);
                if (val & 1)
                        pclog("ENABLE_EMS ");
                if (val & 2)
                        pclog("ENABLE_XMS ");
                if (val & 4)
                        pclog("640K ");
                if (val & 8)
                        pclog("X8X ");
                if (val & 16)
                        pclog("UPPER_IS_XMS ");
                pclog("\n");
        }
        /* Bit 2 controls size of conventional memory */
        if (val & 4) {
                t3100e_ems.upper_base = 0xA0000;
                t3100e_ems.upper_pages = 24;
        } else {
                t3100e_ems.upper_base = 0x80000;
                t3100e_ems.upper_pages = 32;
        }
        upper_len = t3100e_ems.upper_pages * 16384;

        mem_mapping_set_addr(&ram_low_mapping, 0, t3100e_ems.upper_base);
        /* Bit 0 set if upper RAM is EMS */
        t3100e_ems.upper_is_ems = (val & 1);

        /* Bit 1 set if high RAM is enabled */
        if (val & 2) {
                mem_mapping_enable(&ram_high_mapping);
        } else {
                mem_mapping_disable(&ram_high_mapping);
        }

        /* Bit 4 set if upper RAM is mapped to high memory
         * (and bit 1 set if XMS enabled) */
        if ((val & 0x12) == 0x12) {
                mem_mapping_set_addr(&t3100e_ems.upper_mapping, mem_size * 1024, upper_len);
                mem_mapping_enable(&t3100e_ems.upper_mapping);
                mem_mapping_set_exec(&t3100e_ems.upper_mapping, ram + t3100e_ems.upper_base);
        } else {
                mem_mapping_disable(&t3100e_ems.upper_mapping);
        }
        /* Recalculate EMS mappings */
        for (n = 0; n < 4; n++) {
                t3100e_ems_out(t3100e_ems_page_reg[n], t3100e_ems.page[n], &t3100e_ems);
        }

        // dump_mappings();
}

void t3100e_notify_set(uint8_t value) { t3100e_ems.notify = value; }

void t3100e_mono_set(uint8_t value) { t3100e_ems.mono = value; }

uint8_t t3100e_mono_get(void) { return t3100e_ems.mono; }

void t3100e_turbo_set(uint8_t value) {
        t3100e_ems.turbo = value;
        if (!value)
                cpu_set_turbo(0);
        else
                cpu_set_turbo(1);
}

uint8_t t3100e_sys_in(uint16_t addr, void *p) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)p;

        /* The low 4 bits always seem to be 0x0C. The high 4 are a
         * notification sent by the keyboard controller when it detects
         * an [Fn] key combination */
        if (t3100e_log)
                pclog("IN 0x8084\n");
        return 0x0C | (regs->notify << 4);
}

/* Handle writes to the T3100e system control port at 0x8084 */
void t3100e_sys_out(uint16_t addr, uint8_t val, void *p) {
        //	struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)p;

        switch (val & 0xE0) {
        case 0x00: /* Set serial port IRQs. Not implemented */
                if (t3100e_log)
                        pclog("OUT 0x8084, %02x [ set serial port IRQs]\n", val);
                break;
        case 0x40: /* Set RAM mappings. */
                t3100e_map_ram(val & 0x1F);
                break;

        case 0x80: /* Set video options. */
                t3100e_video_options_set(val & 0x1F);
                break;

                /* Other options not implemented. */
        default:
                if (t3100e_log)
                        pclog("OUT 0x8084, %02x\n", val);
                break;
        }
}

uint8_t t3100e_config_get(void) {
        /* The byte returned:
                Bit 7: Set if internal plasma display enabled
                Bit 6: Set if running at 6MHz, clear at full speed
                Bit 5: Always 1?
                Bit 4: Set if the FD2MB jumper is present (internal floppy is ?tri-mode)
                Bit 3: Clear if the FD2 jumper is present (two internal floppies)
                Bit 2: Set if the internal drive is A:, clear if B:
                Bit 1: Set if the parallel port is configured as a floppy connector
                       for the second drive.
                Bit 0: Set if the F2HD jumper is present (internal floppy is 720k)
         */
        uint8_t value = 0x28; /* Start with bits 5 and 3 set. */

        int type_a = fdd_get_type(0);
        int type_b = fdd_get_type(1);
        int prt_switch; /* External drive type: 0=> none, 1=>A, 2=>B */

        /* Get display setting */
        if (t3100e_display_get())
                value |= 0x80;
        if (!t3100e_ems.turbo)
                value |= 0x40;

        /* Try to determine the floppy types.*/

        prt_switch = (type_b ? 2 : 0);
        switch (type_a) {
                /* Since a T3100e cannot have an internal 5.25" drive, mark 5.25" A: drive as
                 * being external, and set the internal type based on type_b. */
        case 1:                 /* 360k */
        case 2:                 /* 1.2Mb */
        case 3:                 /* 1.2Mb RPMx2*/
                prt_switch = 1; /* External drive is A: */
                switch (type_b) {
                case 1: /* 360k */
                case 4:
                        value |= 1;
                        break; /* 720k */
                case 6:
                        value |= 0x10;
                        break; /* Tri-mode */
                               /* All others will be treated as 1.4M */
                }
                break;
        case 4:
                value |= 0x01; /* 720k */
                if (type_a == type_b) {
                        value &= (~8);  /* Two internal drives */
                        prt_switch = 0; /* No external drive */
                }
                break;
        case 5: /* 1.4M */
        case 7: /* 2.8M */
                if (type_a == type_b) {
                        value &= (~8);  /* Two internal drives */
                        prt_switch = 0; /* No external drive */
                }
                break;
        case 6: /* 3-mode */
                value |= 0x10;
                if (type_a == type_b) {
                        value &= (~8);  /* Two internal drives */
                        prt_switch = 0; /* No external drive */
                }
                break;
        } /* End switch */
        switch (prt_switch) {
        case 0:
                value |= 4;
                break; /* No external floppy */
        case 1:
                value |= 2;
                break; /* External floppy is A: */
        case 2:
                value |= 6;
                break; /* External floppy is B: */
        }
        return value;
}

/* Read EMS page register */
uint8_t t3100e_ems_in(uint16_t addr, void *p) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)p;

        return regs->page[port_to_page(addr)];
}

/* Write EMS page register */
void t3100e_ems_out(uint16_t addr, uint8_t val, void *p) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)p;
        int pg = port_to_page(addr);

        regs->page_exec[pg & 3] = t3100e_ems_execaddr(regs, pg, val);
        if (t3100e_log)
                pclog("EMS: page %d %02x -> %02x [%06x]\n", pg, regs->page[pg], val, regs->page_exec[pg & 3]);
        regs->page[pg] = val;

        pg &= 3;
        /* Bit 7 set if page is enabled, reset if page is disabled */
        if (regs->page_exec[pg]) {
                if (t3100e_log)
                        pclog("Enabling EMS RAM at %05x\n", page_to_addr(pg));
                mem_mapping_enable(&regs->mapping[pg]);
                mem_mapping_set_exec(&regs->mapping[pg], ram + regs->page_exec[pg]);
        } else {
                if (t3100e_log)
                        pclog("Disabling EMS RAM at %05x\n", page_to_addr(pg));
                mem_mapping_disable(&regs->mapping[pg]);
        }
}

/* Read RAM in the EMS page frame */
static uint8_t ems_read_ram(uint32_t addr, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;
        int pg = addr_to_page(addr);

        if (pg < 0)
                return 0xFF;
        addr = regs->page_exec[pg] + (addr & 0x3FFF);
        return ram[addr];
}

static uint16_t ems_read_ramw(uint32_t addr, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;
        int pg = addr_to_page(addr);

        if (pg < 0)
                return 0xFF;
        // pclog("ems_read_ramw addr=%05x ", addr);
        addr = regs->page_exec[pg] + (addr & 0x3FFF);
        // pclog("-> %06x val=%04x\n", addr, *(uint16_t *)&ram[addr]);
        return *(uint16_t *)&ram[addr];
}

static uint32_t ems_read_raml(uint32_t addr, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;
        int pg = addr_to_page(addr);

        if (pg < 0)
                return 0xFF;
        addr = regs->page_exec[pg] + (addr & 0x3FFF);
        return *(uint32_t *)&ram[addr];
}

/* Write RAM in the EMS page frame */
static void ems_write_ram(uint32_t addr, uint8_t val, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;
        int pg = addr_to_page(addr);

        if (pg < 0)
                return;
        addr = regs->page_exec[pg] + (addr & 0x3FFF);
        ram[addr] = val;
}

static void ems_write_ramw(uint32_t addr, uint16_t val, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;
        int pg = addr_to_page(addr);

        if (pg < 0)
                return;
        // pclog("ems_write_ramw addr=%05x ", addr);
        addr = regs->page_exec[pg] + (addr & 0x3FFF);
        // pclog("-> %06x val=%04x\n", addr, val);

        *(uint16_t *)&ram[addr] = val;
}

static void ems_write_raml(uint32_t addr, uint32_t val, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;
        int pg = addr_to_page(addr);

        if (pg < 0)
                return;
        addr = regs->page_exec[pg] + (addr & 0x3FFF);
        *(uint32_t *)&ram[addr] = val;
}

/* Read RAM in the upper area. This is basically what the 'remapped'
 * mapping in mem.c does, except that the upper area can move around */
static uint8_t upper_read_ram(uint32_t addr, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;

        addr = (addr - (1024 * mem_size)) + regs->upper_base;
        return ram[addr];
}

static uint16_t upper_read_ramw(uint32_t addr, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;

        addr = (addr - (1024 * mem_size)) + regs->upper_base;
        return *(uint16_t *)&ram[addr];
}

static uint32_t upper_read_raml(uint32_t addr, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;

        addr = (addr - (1024 * mem_size)) + regs->upper_base;
        return *(uint32_t *)&ram[addr];
}

static void upper_write_ram(uint32_t addr, uint8_t val, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;

        addr = (addr - (1024 * mem_size)) + regs->upper_base;
        ram[addr] = val;
}

static void upper_write_ramw(uint32_t addr, uint16_t val, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;

        addr = (addr - (1024 * mem_size)) + regs->upper_base;
        *(uint16_t *)&ram[addr] = val;
}

static void upper_write_raml(uint32_t addr, uint32_t val, void *priv) {
        struct t3100e_ems_regs *regs = (struct t3100e_ems_regs *)priv;

        addr = (addr - (1024 * mem_size)) + regs->upper_base;
        *(uint32_t *)&ram[addr] = val;
}

void at_t3100e_init() {
        int pg;

        memset(&t3100e_ems, 0, sizeof(t3100e_ems));

        at_init();
        /* Hook up system control port */
        io_sethandler(0x8084, 0x0001, t3100e_sys_in, NULL, NULL, t3100e_sys_out, NULL, NULL, &t3100e_ems);

        /* Start monitoring all 16 EMS registers */
        for (pg = 0; pg < 16; pg++) {
                io_sethandler(t3100e_ems_page_reg[pg], 0x0001, t3100e_ems_in, NULL, NULL, t3100e_ems_out, NULL, NULL,
                              &t3100e_ems);
        }

        /* Map the EMS page frame */
        for (pg = 0; pg < 4; pg++) {
                if (t3100e_log)
                        pclog("Adding memory map at %x for page %d\n", page_to_addr(pg), pg);
                mem_mapping_add(&t3100e_ems.mapping[pg], page_to_addr(pg), 16384, ems_read_ram, ems_read_ramw, ems_read_raml,
                                ems_write_ram, ems_write_ramw, ems_write_raml, NULL, MEM_MAPPING_EXTERNAL, &t3100e_ems);
                /* Start them all off disabled */
                mem_mapping_disable(&t3100e_ems.mapping[pg]);
        }
        /* Mapping for upper RAM when in use as XMS*/
        mem_mapping_add(&t3100e_ems.upper_mapping, mem_size * 1024, 384 * 1024, upper_read_ram, upper_read_ramw, upper_read_raml,
                        upper_write_ram, upper_write_ramw, upper_write_raml, NULL, MEM_MAPPING_INTERNAL, &t3100e_ems);
        mem_mapping_disable(&t3100e_ems.upper_mapping);
        mem_mapping_disable(&ram_remapped_mapping);
}
